package webv1

import (
	"bytes"
	"crypto/sha1"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"github.com/gin-gonic/gin"
	"github.com/h2non/filetype"
	"go.uber.org/zap"
	"image"
	"image/png"
	"io"
	"io/ioutil"
	"myCloud/contrib"
	"myCloud/dbmodel"
	"myCloud/dbopr"
	"myCloud/notify"
	"myCloud/repo"
	"myCloud/thumb"
	"myCloud/utils"
	"net/http"
	"strings"
)

const (
	MB             = 1024 * 1024
	fileRecordKey  = "fileRecordKey"
	fileRawDataKey = "fileRawDataKey"
)

func albumV2ApiInit(r *gin.RouterGroup) {
	s := r.Group("/server")
	// 获取本终端ID
	s.PUT("/device", check(fetchDeviceRequest{}), fetchDeviceHandler)

	g := r.Group("/dcim")
	// 磁盘统计信息
	g.GET("/disk/info", diskInfoHandler)

	devs := g.Group("/devices")
	// 终端列表
	devs.GET("/", userDeviceListsHandler)
	device := devs.Group("/:devID", contrib.SetDeviceInfo)
	// 单个终端详细信息接口
	device.GET("/info", deviceInfoHandler)

	// 相册管理类接口是重点
	albums := g.Group("/albums")

	// 相册列表，返回所有终端的所有相册列表，客户端可以酌情显示，不分页
	albums.GET("/", allAlbumListsHandler)

	// 检查某个相册是否存在 若存在返回，若不存在应新增并返回
	albums.PUT("/", check(fetchAlbumRequest{}), fetchAlbumHandler)

	album := albums.Group("/:albumID", contrib.SetAlbumInfo)

	// 获取单个相册的详细信息
	album.GET("/", albumDetailInfoHandler)
	// 获取指定相册下所有文件
	album.GET("/files", albumFileListHandler)

	// 文件上传接口，上传到指定相册
	// 快速上传，适用于图片或视频
	album.PUT("/upload/quick", check(uploadRequest{}), quickUploadHandler, createFileRecord)
	album.PUT("/upload/full/picture",
		check(uploadFormRequest{}),
		uploadWithSizeLimit(512, 20*MB),
		checkFileType("image"),
		createFileRecord,
		newFileUploadNotify,
	)
	album.PUT("/upload/full/video",
		check(uploadFormRequest{}),
		uploadWithSizeLimit(512, 1024*MB),
		checkFileType("video"),
		createFileRecord,
	)
	// 分批次上传
	album.PUT("/upload/part")

	files := g.Group("/files")
	file := files.Group("/:fileID", contrib.SetFileInfo)
	file.DELETE("/", deleteFileHandler)
	// 某个文件的信息
	file.GET("/info", fileDetailInfoHandler)
	// 文件缩略图
	file.GET("/thumb/:size", fileThumbHandler)
	// 原始文件内容
	file.GET("/raw", setRawFile, fileRawDataHandler)
	// 可选，按固定格式返回
	file.GET("/raw.png", setRawFile, pngFileHandler)
	// 可选，视频推流
	//file.GET("/video/stream", streamFileHandler)
}

type fetchDeviceRequest struct {
	Brand string `json:"brand" validate:"required"`
	Model string `json:"model" validate:"required"`
	Hash  string `json:"hash" validate:"required"`
}

func fetchDeviceHandler(c *gin.Context) {
	user := getUser(c)
	req := getRequest(c).(*fetchDeviceRequest)
	ud := dbopr.NewUserDeviceProvider()

	r, err := ud.FindOrCreate(int(user.ID), req.Brand, req.Model, req.Hash)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "获取用户终端失败", err)
	}
	c.JSON(http.StatusOK, gin.H{"device_id": r.ID})
}

type diskInfoResponse struct {
	Size    int `json:"size"`
	Used    int `json:"used"`
	Avail   int `json:"avail"`
	Total   int `json:"total"`
	Picture int `json:"picture"`
	Video   int `json:"video"`
}

/*
diskInfoHandler 返回磁盘信息，包括已用空间、剩余空间; 磁盘信息使用API直接从系统 获取
同时返回用户统计信息，文件总数、图片总数、视频总数、其他文件总数 统计信息从数据库获取
文件上传时设置到数据库中
*/
func diskInfoHandler(c *gin.Context) {
	info, err := utils.GetFstatInfo()
	if err != nil {
		abReq(c, http.StatusInternalServerError, "获取磁盘信息失败", err)
	}

	var out = &diskInfoResponse{
		Size:  info.Total,
		Used:  info.Used,
		Avail: info.Avail,
	}
	c.JSON(http.StatusOK, out)
}

type deviceInfo struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Brand string `json:"brand"`
	Model string `json:"model"`
	Hash  string `json:"hash"`

	Albums   int `json:"albums"`
	Pictures int `json:"pictures"`
	Videos   int `json:"videos"`
}

func userDeviceListsHandler(c *gin.Context) {
	user := getUser(c)
	udProvider := dbopr.NewUserDeviceProvider()

	rs, err := udProvider.ListDevices(int(user.ID))
	if err != nil {
		abReq(c, http.StatusInternalServerError, "列举用户终端失败", err)
		return
	}

	var out = make([]deviceInfo, 0)
	for _, item := range rs {
		var cur = deviceInfo{
			ID:       int(item.ID),
			Name:     item.Name,
			Brand:    item.Brand,
			Model:    item.DevModel,
			Hash:     item.Hash,
			Albums:   0,
			Pictures: 0,
			Videos:   0,
		}
		out = append(out, cur)
	}

	c.JSON(http.StatusOK, out)
}

func deviceInfoHandler(c *gin.Context) {
	dev := contrib.GetUserDevice(c)
	var out = &deviceInfo{
		ID:       int(dev.ID),
		Name:     dev.Name,
		Brand:    dev.Brand,
		Model:    dev.DevModel,
		Hash:     dev.Hash,
		Albums:   0,
		Pictures: 0,
		Videos:   0,
	}
	c.JSON(http.StatusOK, out)
}

type albumInfo struct {
	ID         int    `json:"id"`
	DeviceID   int    `json:"device_id"`
	DeviceName string `json:"device_name"`
	Name       string `json:"name"`
	CreatedAt  string `json:"created_at"`
	LastModify string `json:"last_modify"`
	Thumb      string `json:"thumb"`
}

// 获取所有设备中断下的相册，不分页
func allAlbumListsHandler(c *gin.Context) {
	user := getUser(c)
	p := dbopr.NewAlbumProvider()

	rs, err := p.GetByUser(int(user.ID))
	if err != nil {
		abReq(c, http.StatusInternalServerError, "获取用户相册出错", err)
		return
	}

	var out = make([]albumInfo, 0)
	for _, item := range rs {
		var cur = albumInfo{
			ID:         int(item.ID),
			DeviceID:   item.DevID,
			Name:       item.Name,
			CreatedAt:  utils.Timetostr(item.CreatedAt),
			LastModify: utils.Timetostr(item.UpdatedAt),
		}
		if item.Thumb != "" {
			thumbData, err := thumb.Instance().Read(item.Thumb)
			if err == nil {
				cur.Thumb = base64.StdEncoding.EncodeToString(thumbData)
			}
		}
		out = append(out, cur)
	}

	c.JSON(http.StatusOK, out)
}

type fetchAlbumRequest struct {
	DeviceID int    `json:"device_id" validate:"required"`
	Name     string `json:"name" validate:"required"`
}

func fetchAlbumHandler(c *gin.Context) {
	user := getUser(c)
	req := getRequest(c).(*fetchAlbumRequest)
	p := dbopr.NewAlbumProvider()

	r, err := p.FindOrCreate(int(user.ID), req.DeviceID, req.Name)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "查找或创建相册失败", err)
		return
	}

	c.JSON(http.StatusOK, gin.H{"album_id": r.ID})
}

func albumDetailInfoHandler(c *gin.Context) {
	album := contrib.GetUserAlbum(c)

	var out = &albumInfo{
		ID:         int(album.ID),
		DeviceID:   album.DevID,
		DeviceName: "",
		Name:       album.Name,
		CreatedAt:  utils.Timetostr(album.CreatedAt),
		LastModify: utils.Timetostr(album.UpdatedAt),
	}
	if album.Thumb != "" {
		thumbData, err := thumb.Instance().Read(album.Thumb)
		if err == nil {
			out.Thumb = base64.StdEncoding.EncodeToString(thumbData)
		}
	}

	c.JSON(http.StatusOK, out)
}

type fileInfo struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Type string `json:"type"`
	Thumb string `json:"thumb"`
}

func albumFileListHandler(c *gin.Context) {
	album := contrib.GetUserAlbum(c)
	p := dbopr.NewFileProvider()

	rs, err := p.FindByAlbum(int(album.ID))
	if err != nil {
		abReq(c, http.StatusInternalServerError, "加载相册文件列表失败", err)
		return
	}

	var out = make([]fileInfo, 0)
	for _, item := range rs {
		var cur = fileInfo{
			ID:    int(item.ID),
			Type: item.CType,
			Name:  item.Name,
		}
		tData, err := thumb.Instance().Read(item.Hash)
		if err == nil {
			cur.Thumb = base64.StdEncoding.EncodeToString(tData)
		}
		out = append(out, cur)
	}
	c.JSON(http.StatusOK, out)
}

type uploadRequest struct {
	FileName string `json:"file_name" validate:"required"`
	Hash     string `json:"hash" validate:"required"`
}

/*
quickUploadHandler 快速上传
1，检查 hash 值，若有则直接保存文件名信息，相册归属信息
2，若无，则返回失败
*/
func quickUploadHandler(c *gin.Context) {
	album := contrib.GetUserAlbum(c)
	user := getUser(c)
	req := getRequest(c).(*uploadRequest)
	p := dbopr.NewFileProvider()

	obj, notFound, err := p.GetByHash(req.Hash)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "查找文件失败", err)
		return
	}
	if notFound {
		abReq(c, http.StatusNotFound, "快速上传失败，请上传完整文件")
		return
	}

	// 如果同相册下已有此文件，返回上传已成功
	if int(album.ID) == obj.AlbumID {
		c.JSON(http.StatusCreated, gin.H{"file_id": obj.ID})
		c.Abort()
		return
	}

	// 否则保存新的文件信息
	r := &dbmodel.File{
		UserID:  int(user.ID),
		AlbumID: int(album.ID),
		Hash:    req.Hash,
		Size:    obj.Size,
		Name:    req.FileName,
		CType:   obj.CType,
		Thumb:   obj.Thumb,
	}
	c.Set(fileRecordKey, r)
	c.Next()
}

func createFileRecord(c *gin.Context) {
	r := c.MustGet(fileRecordKey).(*dbmodel.File)
	p := dbopr.NewFileProvider()
	err := p.Create(r)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "创建文件记录失败", err)
		return
	}
	c.JSON(http.StatusCreated, gin.H{"file_id": r.ID})
	c.Next()
}

func newFileUploadNotify(c *gin.Context) {
	r := c.MustGet(fileRecordKey).(*dbmodel.File)
	notify.FileUploadFinished(int(r.ID))
	c.Next()
}

// uploadFormRequest 与上面的 uploadRequest 唯一区别在于 form 与 json
type uploadFormRequest struct {
	FileName string `form:"file_name" validate:"required"`
	Hash     string `form:"hash" validate:"required"`
}

func uploadWithSizeLimit(min, max int) gin.HandlerFunc {
	return func(c *gin.Context) {
		user := getUser(c)
		album := contrib.GetUserAlbum(c)
		req := getRequest(c).(*uploadFormRequest)
		p := dbopr.NewFileProvider()

		key, err := repo.HashKeyFromString(req.Hash)
		if err != nil {
			abReq(c, http.StatusBadRequest, "文件哈希解析错误", err)
			return
		}

		_, notFound, err := p.GetByHash(req.Hash)
		if err != nil {
			abReq(c, http.StatusInternalServerError, "查找文件失败", err)
			return
		}
		if !notFound {
			// 如果找到结果，应走快速上传流程
			abReq(c, http.StatusBadRequest, "应使用快速上传")
			return
		}

		f, header, err := c.Request.FormFile("file")
		if err != nil {
			abReq(c, http.StatusBadRequest, "未找到文件内容", err)
			return
		}

		if header.Size < int64(min) {
			abReq(c, http.StatusBadRequest, "文件过小")
			return
		}

		if header.Size > int64(max) {
			abReq(c, http.StatusBadRequest, "文件过大")
			return
		}

		var readLength = 500
		var headBuf = make([]byte, readLength)
		readed, err := f.Read(headBuf)
		if err != nil {
			abReq(c, http.StatusInternalServerError, "文件头读取失败", err)
			return
		}
		if readed != readLength {
			abReq(c, http.StatusInternalServerError, "文件头读取字节数不一致")
			return
		}

		kind, err := filetype.Match(headBuf)
		if err != nil {
			abReq(c, http.StatusInternalServerError, "文件内容检测失败", err)
			return
		}
		cType := kind.MIME.Value
		println(cType)

		_, err = f.Seek(0, 0)
		if err != nil {
			abReq(c, http.StatusInternalServerError, "文件内容读取失败", err)
			return
		}

		/*
		可选，检查客户端上传的hash是否正确
		*/
		v := checkFileHash(f, int(header.Size), req.Hash)
		if v == false {
			abReq(c, http.StatusBadRequest, "文件哈希校验出错")
			return
		}
		_, err = f.Seek(0, 0)
		if err != nil {
			abReq(c, http.StatusInternalServerError, "文件内容读取失败", err)
			return
		}

		storage := repo.Instance()
		i, ok := storage.(repo.FileIoStorage)
		if ok {
			err = i.AddFrom(f, key)
		} else {
			body, err := ioutil.ReadAll(f)
			if err != nil {
				abReq(c, http.StatusBadRequest, "读取上传文件失败", err)
				return
			}
			err = storage.Add(body, key)
		}
		if err != nil {
			abReq(c, http.StatusInternalServerError, "写入文件失败", err)
			return
		}

		// 文件写入后，应生成文件对象
		r := &dbmodel.File{
			UserID:  int(user.ID),
			AlbumID: int(album.ID),
			Size:    int(header.Size),
			Hash:    req.Hash,
			CType:   cType,
			Name:    req.FileName,
		}
		c.Set(fileRecordKey, r)
		c.Next()
	}
}

func checkFileType(cTypePrefix string) gin.HandlerFunc {
	return func(c *gin.Context) {
		fileKey := c.MustGet(fileRecordKey).(*dbmodel.File)
		if !strings.HasPrefix(strings.ToLower(fileKey.CType), cTypePrefix) {
			abReq(c, http.StatusBadRequest, "只能上传图片")
			return
		}

		c.Next()
	}
}


func checkFileHash(f io.Reader, size int, hash string) bool {
	var h = make([]byte, 28)

	p := sha1.New()
	_, err := io.Copy(p, f)
	if err != nil {
		return false
	}
	h1 := p.Sum(nil)
	copy(h, h1)

	binary.BigEndian.PutUint32(h[20:], uint32(size))
	copy(h[24:], []byte{0, 0, 0, 0})
	var hashComputed = strings.ToLower(hex.EncodeToString(h))
	logger.Infow("hash check", zap.String("request", hash), zap.String("computed", hashComputed))
	return hash == hashComputed
}

type PictureExifInfo struct {
}

type fileDetailInfo struct {
	ID       int              `json:"id"`
	AlbumID  int              `json:"album_id"`
	Name     string           `json:"name"`
	UploadAt string           `json:"upload_at"`
	Hash     string           `json:"hash"`
	Size     int              `json:"size"`
	Exif     *PictureExifInfo `json:"exif"`
}

func fileDetailInfoHandler(c *gin.Context) {
	file := contrib.GetUserFile(c)

	var out = &fileDetailInfo{
		ID:       int(file.ID),
		AlbumID:  file.AlbumID,
		Name:     file.Name,
		UploadAt: utils.Timetostr(file.CreatedAt),
		Hash:     file.Hash,
		Size:     file.Size,
		Exif:     nil,
	}
	c.JSON(http.StatusOK, out)
}

func fileThumbHandler(c *gin.Context) {
	file := contrib.GetUserFile(c)

	t, err := thumb.Instance().Read(file.Hash)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "获取缩略图失败", err)
		return
	}

	c.Data(http.StatusOK, "image/jpeg", t)
}

func setRawFile(c *gin.Context) {
	file := contrib.GetUserFile(c)

	key, err := repo.HashKeyFromString(file.Hash)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "解析文件内容出错", err)
		return
	}

	body, err := repo.Instance().Get(key)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "读取文件出错", err)
		return
	}

	c.Set(fileRawDataKey, body)
	c.Next()
}

func fileRawDataHandler(c *gin.Context) {
	body := c.MustGet(fileRawDataKey).([]byte)
	file := contrib.GetUserFile(c)
	c.Data(http.StatusOK, file.CType, body)
}

// pngFileHandler 对于图片，固定以 png 格式返回，非图片给事返回一张 固定的错误图片？
func pngFileHandler(c *gin.Context) {
	body := c.MustGet(fileRawDataKey).([]byte)
	file := contrib.GetUserFile(c)
	if strings.ToLower(file.CType) == "image/png" {
		c.Data(http.StatusOK, file.CType, body)
		return
	}

	img, _, err := image.Decode(bytes.NewReader(body))
	if err != nil {
		abReq(c, http.StatusInternalServerError, "图像文件解析失败", err)
		return
	}

	buf := new(bytes.Buffer)
	err = png.Encode(buf, img)
	if err != nil {
		abReq(c, http.StatusInternalServerError, "PNG 解析失败", err)
		return
	}

	c.Data(http.StatusOK, "image/png", buf.Bytes())
}

// deleteFileHandler 仅删除数据库记录，另行提供清理接口
func deleteFileHandler(c *gin.Context) {
	file := contrib.GetUserFile(c)
	p := dbopr.NewFileProvider()

	err := p.RemoveFile(int(file.ID))
	if err != nil {
		abReq(c, http.StatusInternalServerError, "文件删除失败", err)
		return
	}

	c.JSON(http.StatusOK, gin.H{"msg": "OK"})
}
